Skip to content

fix(metadata-core): derive the ADR-0106 D4 read exemption from the #6603 write gate (#7020) - #7361

Merged
os-help merged 2 commits into
mainfrom
claude/issue-7020-d4-read-set-derivation
Aug 10, 2026
Merged

fix(metadata-core): derive the ADR-0106 D4 read exemption from the #6603 write gate (#7020)#7361
os-help merged 2 commits into
mainfrom
claude/issue-7020-d4-read-set-derivation

Conversation

@os-help

@os-help os-help commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Part of #7020

Part of, not Fixes, and the reason is the ruling's own precondition: phase 1's category (b) came back non-empty. Three principals are D4-exempt today while holding no write capability, and the maintainer ruled those named cases go back for a follow-up ruling instead of being silently unified over. That follow-up has not happened, so merging this must not close the card.

The ruling

Maintainer, 2026-08-10, issue comment 5236144046 — quoted verbatim:

Direction: the write-capability gate (#6603) is the authoritative set; the ADR-0106 D4 read-exemption list becomes a derivation of it, not a second hand-kept list — "whoever can write a schema can see all of it" must be enforced by construction, not by two lists staying coincidentally equal.

Hard precondition: the first deliverable is the measured diff of the two sets as they exist today. If the diff surfaces intentional read-only exemptions (a principal meant to read schemas it cannot write), bring those named cases back for a follow-up ruling instead of silently unifying over them.

Phase 1 — the measured diff (posted before implementing)

Full table with file:line evidence: issue comment 5236962689. Summary, measured on merge base 06be54ec3 across every shipped and example permission set:

(a) satisfies the write gate but NOT D4-exempt — empty among seeded permission sets, non-empty in the principal space the platform admits and already tests. meta-item-save-capability-gate.test.ts:261 and meta-compound-save-and-reset-capability-gate.test.ts:239 both pin a manage_metadata-only caller as a 200 through the write gate, and runtime/src/sandbox/script-user-type-assertions.ts:72 constructs exactly that principal. Such a caller passed every write gate and still read a projected schema — the GET, edit and PUT round trip that deletes the fields it could not see.

(b) D4-exempt but NOT satisfying the write gate — non-empty (3): organization_admin, organization_admin_no_bypass, showcase_ops. These are intentional read-only exemptions and the intent is stated in the source, not inferred: default-permission-sets.ts:139-142 withholds studio.access and manage_metadata from the org admin in as many words while granting setup.access. They go back for the follow-up ruling; this PR preserves their behaviour exactly.

Phase 2 — the derivation

packages/metadata-core/src/object-schema-fls.ts now names the two halves and builds the exemption from them:

OBJECT_SCHEMA_WRITE_CAPABILITIES            // ['manage_metadata'] — the #6603 gate, spelled ONCE
OBJECT_SCHEMA_READ_ONLY_EXEMPT_CAPABILITIES // ['studio.access','setup.access'] — the named read-only exemptions
OBJECT_SCHEMA_MASK_EXEMPT_CAPABILITIES      // = write ∪ read-only, DERIVED

The derivation is one-directional by construction: can-write implies can-see-all, and no principal loses read access. Narrowing the read-only half before the follow-up ruling would be the reversal the ruling forbids, so it is preserved verbatim and pinned as such.

One structural finding shaped the spelling. OBJECT_SCHEMA_MASK_EXEMPT_CAPABILITIES was not only the D4 mask exemption — rest/src/package-routes.ts and runtime/src/domains/packages.ts import the same constant as the /packages read cohort, which was ruled separately (#7033 / #7023) and deliberately pins write-only callers OUT (package-envelope.conformance.test.ts:518). Widening the constant in place would have re-ruled that gate as a side effect. Both call sites now name OBJECT_SCHEMA_READ_ONLY_EXEMPT_CAPABILITIES, whose value is identical to what they read before — behaviour preserved, no re-ruling by side effect.

scripts/adr-anchors/packages__metadata-core__src__object-schema-fls.ts.json is amended too: its invariant told the next author that widening the exemption reverses a recorded decision, which is now only half true. It records the ruling, the derivation, and the two NEW reversals (re-spelling either half as a second literal; narrowing the read-only half before the follow-up ruling).

The ADR text amendment is deliberately NOT in this PR

The first push carried an amendment to ADR-0106's D4 section. check-adr-merge-approval refused it, correctly:

「adr 只能由维护者自己确认,人工合并,ai 不得擅自合并。」 (#6741, maintainer, verbatim)

Drafting is allowed and only the merge is reserved, but the amendment is not required by the fix, and #7020 stays open for the follow-up ruling that will settle D4's final wording anyway — so it was removed in the second commit rather than parking this PR behind a personal merge. The governance record the next code author actually reads (the anchor invariant above) carries the amendment. Suggested D4 wording for whoever lands the ADR change: the exemption set is derived as OBJECT_SCHEMA_WRITE_CAPABILITIES UNION OBJECT_SCHEMA_READ_ONLY_EXEMPT_CAPABILITIES; the derivation is one-directional; organization_admin / organization_admin_no_bypass / showcase_ops are the named read-only exemptions pending the follow-up ruling; the /packages read cohort (#7033 / #7023) is unchanged.

Tests

New coverage, all three cases the card asked for:

  • (i) the broken case — a manage_metadata holder with neither builder capability reads the FULL schema. Added to the shared ADR-0106 contract table (object-schema-fls-contract.ts, case write-capable-caller/exempt), so it drives all thirteen schema-serving exits in rest and runtime rather than one unit.
  • (ii) existing exempt principals unchanged — the admin_full_access shape, the organization_admin shape and the showcase_ops shape all still exempt.
  • (iii) a principal with neither half still gets the masked view; adjacent capability names do not leak in.
  • Plus: the posture resolves to exempt without consulting the security service, and the union is asserted to BE the concatenation of its two halves — a third hand-kept list cannot be reintroduced quietly.

Reverse verification (predicted direction: red — the derivation is the whole behaviour). Restoring the union to the read-only half alone:

metadata-core   Tests  3 failed | 126 passed (129)
  AssertionError: expected false to be true
  AssertionError: expected [ 'studio.access', 'setup.access' ] to deeply equal [ 'manage_metadata', ...(2) ]
rest            Failed Tests 7   — write-capable-caller/exempt, once per exit, by name
runtime         Failed Tests 6   — same case, same shape

Thirteen exits red by name, and cases (ii)/(iii) stayed green throughout — which is what shows the preserve-behaviour half is not carried by the same line as the fix. The fix was taken out with an edit and restored, then git diff compared byte-for-byte against the saved patch (identical). Green after restore:

@objectstack/metadata-core   Test Files  9 passed (9)      Tests  129 passed (129)
@objectstack/rest            Test Files 77 passed (77)     Tests 1235 passed (1235)
@objectstack/runtime         Test Files 119 passed (119)   Tests 1876 passed (1876)
typecheck (all three)        Done
check:type-check-debt        OK — 33 ledger entries re-measured, none above its recorded number
check-adr-anchors            OK        check-nul-bytes  OK        eslint (changed files)  clean

TEST_DEBT['@objectstack/rest'] did not move: no test file was added to rest or runtime — the new case lives in the shared contract table in metadata-core, which type-checks its tests.

Changeset

minor for @objectstack/metadata-core (two new exported constants), patch for the two consumers (call-site rename, no behaviour change of their own). The behaviour change is a deliberate widening of read access for manage_metadata holders and is the ruled intent, so it is stated in the changeset in those terms rather than buried as a refactor.


Generated by Claude Code

 write gate (#7020)

The maintainer's 2026-08-10 ruling on #7020: the write-capability gate is the
authoritative set and the D4 read-exemption list becomes a derivation of it, so
"whoever can write a schema can see all of it" is enforced by construction
rather than by two hand-kept lists staying coincidentally equal.

The measured diff (posted on #7020 before implementing) found the two sets met
only on `admin_full_access`, which carries `manage_metadata` AND `studio.access`
AND `setup.access`. A `manage_metadata`-only caller — a shape every write gate
admits and pins as a 200 — read a PROJECTED schema, so its GET, edit and PUT
round trip deleted the fields it was never shown.

`OBJECT_SCHEMA_MASK_EXEMPT_CAPABILITIES` is now the union of two named halves:
`OBJECT_SCHEMA_WRITE_CAPABILITIES` (the #6603 key, spelled once) and
`OBJECT_SCHEMA_READ_ONLY_EXEMPT_CAPABILITIES` (`studio.access`/`setup.access`).
The derivation is one-directional — nobody loses read access, and the three
named read-only exemptions the measurement surfaced go back for the follow-up
ruling the card stays open for.

The two `/packages` call sites name the read-only half explicitly: that cohort
was ruled separately (#7033 / #7023) and pins write-only callers out, so its
value is unchanged and #7020 does not re-rule it by side effect.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0158ZQo7LiHSxGWpYKuPq1wu
@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 10, 2026 8:01am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/metadata-core, @objectstack/rest, @objectstack/runtime.

24 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/connect-mcp.mdx (via @objectstack/rest)
  • content/docs/api/client-sdk.mdx (via packages/runtime)
  • content/docs/api/error-handling-server.mdx (via @objectstack/rest)
  • content/docs/api/index.mdx (via @objectstack/rest, @objectstack/runtime)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/runtime)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/metadata-core, @objectstack/runtime)
  • content/docs/concepts/north-star.mdx (via packages/runtime)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/runtime)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/vercel.mdx (via @objectstack/runtime)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/runtime)
  • content/docs/kernel/cluster.mdx (via @objectstack/runtime)
  • content/docs/permissions/authentication.mdx (via @objectstack/rest, @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via packages/runtime)
  • content/docs/permissions/system-context.mdx (via packages/rest, packages/runtime)
  • content/docs/plugins/index.mdx (via @objectstack/rest)
  • content/docs/plugins/packages.mdx (via @objectstack/metadata-core, @objectstack/rest, @objectstack/runtime)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/rest, @objectstack/runtime)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/rest)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/runtime)

3 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/rest, @objectstack/runtime)
  • content/docs/releases/v12.mdx (via @objectstack/metadata-core, @objectstack/rest)
  • content/docs/releases/v17.mdx (via @objectstack/rest, @objectstack/runtime)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Aug 10, 2026
`docs/adr/**` merges are reserved to the maintainer in person —
「adr 只能由维护者自己确认,人工合并,ai 不得擅自合并。」 (#6741) — and
`check-adr-merge-approval` enforces it. The D4 amendment text is not required by
the fix, and #7020 stays open for the follow-up ruling on the named read-only
exemptions, which is the natural moment to settle D4's final wording. The
proposed text is quoted in the PR body for the maintainer to apply.

The governance record the next code author actually reads —
`scripts/adr-anchors/packages__metadata-core__src__object-schema-fls.ts.json` —
still carries the amendment, so the file cannot be edited without being told
that the exemption is now derived rather than hand-kept.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0158ZQo7LiHSxGWpYKuPq1wu
@os-help
os-help marked this pull request as ready for review August 10, 2026 08:59
@os-help
os-help added this pull request to the merge queue Aug 10, 2026
Merged via the queue into main with commit 121852d Aug 10, 2026
27 checks passed
@os-help
os-help deleted the claude/issue-7020-d4-read-set-derivation branch August 10, 2026 09:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants